# This file is part of the AutoMAT distribution (https://bitbucket.com/mahomaho/AutoMAT).
# Copyright (c) 2021 Mattias Holmqvist.
# 
# AutoMAT is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# AutoMAT is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with AutoMAT.  If not, see <https://www.gnu.org/licenses/>.

import itertools as _itertools
_noobj='noobj'


class _defaultVal:
	_obj=None
	def __init__(self,default_val):
		self._obj=default_val
	def __dir__(self):
		return self._obj.__dir__()
	def __repr__(self):
		return self._obj.__repr__()
	def __str__(self):
		return str(self._obj)
	def __getattr__(self,key):
		return self
	def __call__(self,*arg,**kwarg):
		return self
	def __getitem__(self,key):
		return self
	@property
	def __class__(self):
		return type(self._obj)
	def __lt__(self, other):
		return self._obj<other
	def __le__(self, other):
		return self._obj<=other
	def __eq__(self, other):
		return self._obj==other
	def __ne__(self, other):
		return self._obj!=other
	def __gt__(self, other):
		return self._obj>other
	def __ge__(self, other):
		return self._obj>=other

	def __hash__(self):
		return hash(self._obj)
	def __add__(self, other):
		return self._obj+other
	def __sub__(self, other):
		return self._obj-other
	def __mul__(self, other):
		return self._obj*other
	def __truediv__(self, other):
		return self._obj/other
	def __floordiv__(self, other):
		return self._obj//other
	def __mod__(self, other):
		return self._obj%other
	def __divmod__(self, other):
		return divmod(self._obj,other)
	def __pow__(self, other, *modulo):
		return pow(self._obj,float(other),*modulo)
	def __lshift__(self, other):
		return int(self._obj)<<int(other)
	def __rshift__(self, other):
		return int(self._obj)>>int(other)
	def __and__(self, other):
		return int(self._obj)&int(other)
	def __xor__(self, other):
		return int(self._obj)^int(other)
	def __or__(self, other):
		return int(self._obj)|int(other)
	def __radd__(self, other):
		return other+self._obj
	def __rsub__(self, other):
		return other-self._obj
	def __rmul__(self, other):
		return other*self._obj
	def __rtruediv__(self, other):
		return other/self._obj
	def __rfloordiv__(self, other):
		return other//self._obj
	def __rmod__(self, other):
		return other%self._obj
	def __rdivmod__(self, other):
		return divmod(other,self._obj)
	def __rpow__(self, other, *modulo):
		return pow(other,float(other),*modulo)
	def __rlshift__(self, other):
		return int(other)<<int(self._obj)
	def __rrshift__(self, other):
		return int(other)>>int(self._obj)
	def __rand__(self, other):
		return int(other)&int(self._obj)
	def __rxor__(self, other):
		return int(other)^int(self._obj)
	def __ror__(self, other):
		return int(other)|int(self._obj)
	def __neg__(self):
		return -self._obj
	def __abs__(self):
		return abs(self._obj)
	def __invert__(self):
		return 1/self._obj
	def __complex__(self):
		return complex(self._obj)
	def __bool__(self):
		return bool(self._obj)
	def __int__(self):
		return int(self._obj)
	def __float__(self):
		return float(self._obj)
	def __round__(self, *ndigits):
		return round(self._obj,*ndigits)
	def __trunc__(self):
		return trunc(self._obj)
	def __floor__(self):
		return floor(self._obj)
	def __ceil__(self):
		return ceil(self._obj)

class default(_defaultVal):
	__default=None
	def __init__(self,default_val,obj=_noobj):
		self.__default=default_val
		if obj is _noobj:
			# to be removed, this is here for backwards compatibility
			import inspect
			self._obj=type('',(),inspect.currentframe().f_back.f_locals)()
		else:
			self._obj=obj
	def __getattr__(self,key):
		try:
			attr=getattr(self._obj,key)
			if attr!=None:
				return default(self.__default,attr)
		except:
			pass
		return _defaultVal(self.__default)
	def __call__(self,*arg,**kwarg):
		try:
			ret=self._obj(*arg,**kwarg)
			if ret!=None:
				return default(self.__default,ret)
		except:
			pass
		return _defaultVal(self.__default)
	def __getitem__(self,key):
		try:
			item=self._obj[key]
			if item!=None:
				return default(self.__default,item)
		except:
			pass
		return _defaultVal(self.__default)

class Select:
	def __init__(self,containerlist):
		self._contList=containerlist
	def findall(self, function, *args,**kwargs):
		'''method that for every member in the list calls the function with the element as a parameter and returns a new list with all elements where the function returns true.
		It is possible to append extra arguments that will be passed to the function.'''
		return ModelList(list(cont for cont in self._contList if function(cont,*args,**kwargs)).__iter__)
	def foreach(self,function, *args,**kwargs):
		'''method that for every member in the list calls the function with the element as a parameter and returns a new list with elements returned from the function.
		It is possible to append extra arguments that will be passed to the function.'''
		ret=[]
		for cont in self._contList:
			res=function(cont,*args,**kwargs)
			if isinstance(res,ModelList):
				ret.extend(res)
			elif res!=None:
				ret.append(res)
		return ModelList(ret.__iter__)
		#return ModelList(list(function(cont,*args,**kwargs) for cont in self._contList).__iter__)
	def findfirst(self, function,default=None, *args,**kwargs):
		'''method that loops over the list members and returns the first argument where the function returns true.
		It is possible to append extra arguments that will be passed to the function.
		If no element matches the search criteria then an exeption will be raised if no default value is added to the function. If a default value is added, then that value will be returend instead.'''
		for cont in self._contList:
			try:
				if function(cont,*args,**kwargs):
					return cont
			except:
				pass
		if default is not None:
			return default
		raise StopIteration()
	def __dir__(self):
		return list((e for e in object.__dir__(self) if e[0]!='_'))
	def sorted(self, function):
		'''Method that return a sorted version of the list. The sort function takes two list elements and returns true if first element should be sorted after second element, otherwise false'''
		s=list(self._contList)
		s.sort(key=function)
		return ModelList(s.__iter__)

class CallableList:
	def __call__(self, *argv, **kwarg):
		def generator():
			for i in self._g():
				ret = i(*argv, **kwarg)
				if ret != None:
					yield ret
		return ModelList(generator)
	@property
	def __doc__(self):
		return self._g.__doc__
	def __dir__(self):
		return []
	def __init__(self,generator):#todo ,*args,**kwargs):
		self._g=generator
	def __hash__(self):
		vals=tuple(self.__iter__())
		return hash(vals)
	def __iter__(self):
		return self._g()# todo*self._args,**self._kwargs)
	def __getitem__(self,key):
		if isinstance(key,int):
			if key < 0:
				l = list(self)
				key=len(l)+key
				if key < 0:
					return CallableNone
				return l[key]
			return next(_itertools.islice(self.__iter__(), key, None), CallableNone)
		elif isinstance(key,slice):
			def generator():
				for i in list(self)[key]:
					yield i
			return CallableList(generator)
	def index(self,value, *args, **kwargs):
		return next(idx for idx, val in enumerate(self.__iter__()) if val == value)
	def __len__(self):
		return sum(1 for _ in self.__iter__())
	def __contains__(self,val):
		try:
			next((e for e in self if e==val))
			return True
		except:
			return False
	def __bool__(self):
		return True
	def __add__(self,other):
		assert isinstance(other, CallableList)
		def generator():
			for a in self._g():
				yield a
			for a in other:
				yield a
		return CallableList(generator)
	def __repr__(self):
		vals=list(self.__iter__())
		return repr(vals)
	def __str__(self):
		vals=list((str(i) for i in self.__iter__()))
		return str(vals)
	def __eq__(self,other):
		try:
			return hash(other)==hash(self)
		except TypeError:
			try:
				otheriter=iter(other)
				for i in self.__iter__():
					if i!=next(otheriter):
						return False
				try:
					next(otheriter)
				except:
					return True
			except:
				pass
			return False
	def __ne__(self,other):
		return not self==other
	
class ModelList:
	@property
	def __doc__(self):
		return self._g.__doc__
	@property
	def select(self):
		'''Select object contains filter functitons and function that can operate on every single list member'''
		return Select(self)
	def __dir__(self):
		ret=set()
		for e in self.__iter__():
			for d in e.__dir__():
				ret.add(d)
		ret.update(('select','index'))
		return list(ret)
	def __new__(cls, *argv, **kwarg):
		try:
			generator = argv[0]
			first = next(generator())
		except StopIteration:
			# the list is empty. For now, check if ModelList and if so return ModelNone
			if cls is ModelList:
				return CallableNone
			return object.__new__(cls)
		except Exception as e:
			# when inherriting ModelList but init argument does not take a generator as first argument
			return object.__new__(cls)
		if callable(first):
			ret = object.__new__(CallableList)
			ret.__init__(*argv, **kwarg)
			return lambda *argv, **kwarg: ret(*argv, **kwarg)
		return object.__new__(cls)
			#return ModelNone
	def __init__(self,generator):#todo ,*args,**kwargs):
		self._g=generator
	def __hash__(self):
		vals=tuple(self.__iter__())
		if len(vals)==0:
			return hash(None)
		try:
			return hash(vals)
		except:
			raise
	def __iter__(self):
		return self._g()# todo*self._args,**self._kwargs)
	def __getitem__(self,key):
		if isinstance(key,int):
			if key < 0:
				l = list(self)
				key=len(l)+key
				if key < 0:
					return CallableNone
				return l[key]
			return next(_itertools.islice(self.__iter__(), key, None), CallableNone)
		elif isinstance(key,slice):
			def generator():
				for i in list(self)[key]:
					yield i
			return ModelList(generator)
		else:
			try:
				return next((i for i in self._g() if i.shortName.val() == key))
			except:
				raise IndexError(f'{key} is not a valid index')
	def index(self,value, *args, **kwargs):
		return next(idx for idx, val in enumerate(self.__iter__()) if val == value)
	def __len__(self):
		return sum(1 for _ in self.__iter__())
	def __contains__(self,val):
		try:
			next((e for e in self if e==val))
			return True
		except:
			return False
	def __bool__(self):
		return next(self.__iter__(),None) is not None
	def __add__(self,other):
		def generator():
			for a in self._g():
				yield a
			for a in other:
				yield a
		return ModelList(generator)
	def __getattr__(self,val):
		def generator():
			for i in self.__iter__():
				res=getattr(i,val,None)
				if res == None:
					continue
				#if callable(res):
				#	raise AttributeError('Requested member is callable, not allowed')
				if isinstance(res,ModelList):
					for j in res:
						if j == None:
							continue
						yield j
				else:
					yield res
		return ModelList(generator)
	def __repr__(self):
		vals=list(self.__iter__())
		return repr(vals)
	def __str__(self):
		vals=list((str(i) for i in self.__iter__()))
		return str(vals)
	def __eq__(self,other):
		try:
			return hash(other)==hash(self)
		except TypeError:
			try:
				otheriter=iter(other)
				for i in self.__iter__():
					if i!=next(otheriter):
						return False
				try:
					next(otheriter)
				except:
					return True
			except:
				pass
			return False
	def __ne__(self,other):
		return not self==other

class ModelNoneType:
	'''Automat extended none type used to indicate that an element does not exist or has no value'''
	_hash=hash(None)
	def __dir__(self):
		return []
	def __init__(self):
		assert 'ModelNone' not in globals()
	@classmethod
	def __hash__(cls):
		return cls._hash
	@classmethod
	def __iter__(cls):
		return ().__iter__()
	def __getitem__(self,key):
		return CallableNone
	def __len__(self):
		return 0
	def __bool__(self):
		return False
	def __contains__(self,val):
		return False
	def __setattr__(self,name,val):
		raise AttributeError(f'Cannot set attributes on ModelNone')
	def __getattr__(self,val):
		# this function returns ModelNone since it ensures that it still is possible to fetch members from missing model items.
		return CallableNone
	def __repr__(self):
		return 'ModelNone'
	def __str__(self):
		return ''
	@classmethod
	def __eq__(cls,val):
		return hash(val)==cls._hash
	@classmethod
	def __ne__(cls,val):
		return hash(val)!=cls._hash
	def __add__(self, val):
		if hasattr(val, '__iter__'):
			return val
		raise AssertionError('Cannot add ModelNone with object of type ' + val.__cls__.__name__)
	def __instancecheck__(self,cls):
		return issubclass(cls, (ModelNoneType, type(None)))

ModelNone=ModelNoneType()

CallableNone=type('ModelNoneType', (ModelNoneType,),  {'__doc__':ModelNone.__doc__, '__init__': (lambda self: None), '__call__': (lambda *argv, **kwarg: CallableNone)})()
	
class singleiterator:
	__slots__=('_ret')
	def __iter__(self):
		return self
	def __init__(self,ret):
		self._ret=ret
	@property
	def __doc__(self):
		return self._ret.__doc__
	def __next__(self):
		if self._ret is None:
			raise StopIteration()
		ret=self._ret
		self._ret=None
		return ret

def validator(definition):
	'''Decorator to be used to add a validation condition for a BSW configuration parameter. Write your validation condition as:
	@validator(path.to.definition)
	def validator_funct(val):
		assert validation, 'errorstring'
		
	path.to.definition can be replaced with autosar class
	'''
	if isinstance(definition,type):
		def inner(func):
			if not hasattr(definition, '_validator'):
				definition._validator =set()
			definition._validator.add(func)
			return func
		return inner
	else:
		def inner(func):
			definition._validationConds.append(func)
			return func
		return inner

def aggregation_validator(definition):
	'''Decorator to be used to add a validation condition for a BSW configuration parameter group or Autosar aggregation.
	Write your validation condition as:

	@aggregation_validator(path.to.definition)
	def validator_funct(aggregation):
		assert validation, 'errorstring'
	'''
	from . import complexbase
	if isinstance(definition,(complexbase.Element, complexbase.Elements)):
		owner=definition._owner
		def inner(func):
			setattr(owner, '_validate_' + definition._name, func)
			return func
		return inner
	else:
		def inner(func):
			if definition._validationCondGrouped:
				assert definition._validationCondGrouped.__code__.co_code == func.__code__.co_code, f'replacing the aggregation_validator {definition._validationCondGrouped} with {func}'
			definition._validationCondGrouped=func
			return func
		return inner
